﻿//1.课程表
//https://leetcode.cn/problems/course-schedule/description/
//你这个学期必须选修 numCourses 门课程，记为 0 到 numCourses - 1 。
//在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出，其中 prerequisites[i] = [ai, bi] ，表示如果要学习课程 ai 则 必须 先学习课程  bi 。
//例如，先修课程对[0, 1] 表示：想要学习课程 0 ，你需要先完成课程 1 。
//请你判断是否可能完成所有课程的学习？如果可以，返回 true ；否则，返回 false 。
//示例 1：
//输入：numCourses = 2, prerequisites = [[1, 0]]
//输出：true
//解释：总共有 2 门课程。学习课程 1 之前，你需要完成课程 0 。这是可能的。
//示例 2：
//输入：numCourses = 2, prerequisites = [[1, 0], [0, 1]]
//输出：false
//解释：总共有 2 门课程。学习课程 1 之前，你需要先完成​课程 0 ；并且学习课程 0 之前，你还应先完成课程 1 。这是不可能的。
//提示：
//1 <= numCourses <= 2000
//0 <= prerequisites.length <= 5000
//prerequisites[i].length == 2
//0 <= ai, bi < numCourses
//prerequisites[i] 中的所有课程对 互不相同
class Solution {
public:
    bool canFinish(int numCourses, vector<vector<int>>& prerequisites) 
    {
        vector<vector<int>> graph(numCourses);
        vector<int> inDegree(numCourses, 0);

        // 构建图和入度数组
        for (const auto& pre : prerequisites) 
        {
            graph[pre[1]].push_back(pre[0]);
            inDegree[pre[0]]++;
        }

        queue<int> q;
        int count = 0; // 记录已处理的课程数

        // 初始化队列
        for (int i = 0; i < numCourses; ++i) 
            if (inDegree[i] == 0) q.push(i);

        // 拓扑排序
        while (!q.empty()) 
        {
            int current = q.front();
            q.pop();
            count++;

            for (int neighbor : graph[current]) 
            {
                if (--inDegree[neighbor] == 0) 
                {
                    q.push(neighbor);
                }
            }
        }
        // 如果所有课程都能处理，则无环
        return count == numCourses;
    }
};



//2.课程表II
//https://leetcode.cn/problems/course-schedule-ii/description/
//现在你总共有 numCourses 门课需要选，记为 0 到 numCourses - 1。给你一个数组 prerequisites ，其中 prerequisites[i] = [ai, bi] ，表示在选修课程 ai 前 必须 先选修 bi 。
//例如，想要学习课程 0 ，你需要先完成课程 1 ，我们用一个匹配来表示：[0, 1] 。
//返回你为了学完所有课程所安排的学习顺序。可能会有多个正确的顺序，你只要返回 任意一种 就可以了。如果不可能完成所有课程，返回 一个空数组 。
//示例 1：
//输入：numCourses = 2, prerequisites = [[1, 0]]
//输出：[0, 1]
//解释：总共有 2 门课程。要学习课程 1，你需要先完成课程 0。因此，正确的课程顺序为[0, 1] 。
//示例 2：
//输入：numCourses = 4, prerequisites = [[1, 0], [2, 0], [3, 1], [3, 2]]
//输出：[0, 2, 1, 3]
//解释：总共有 4 门课程。要学习课程 3，你应该先完成课程 1 和课程 2。并且课程 1 和课程 2 都应该排在课程 0 之后。
//因此，一个正确的课程顺序是[0, 1, 2, 3] 。另一个正确的排序是[0, 2, 1, 3] 。
//示例 3：
//输入：numCourses = 1, prerequisites = []
//输出：[0]
//提示：
//1 <= numCourses <= 2000
//0 <= prerequisites.length <= numCourses * (numCourses - 1)
//prerequisites[i].length == 2
//0 <= ai, bi < numCourses
//ai != bi
//所有[ai, bi] 互不相同
//使用 Kahn算法（基于BFS的拓扑排序）来解决：
//将课程依赖关系构建为有向图
//计算每个节点的入度
//从入度为0的节点开始处理，逐步"完成"课程
//如果所有课程都能完成，返回拓扑排序结果；否则返回空数组
class Solution {
public:
    vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites) 
    {
        // 1. 构建邻接表表示的有向图
        vector<vector<int>> graph(numCourses);
        // 2. 记录每个课程的入度（前置课程数量）
        vector<int> inDegree(numCourses);

        // 3. 构建图和入度数组
        for (const auto& pre : prerequisites) 
        {
            // pre = [ai, bi] 表示学习ai前需要先学bi
            // 所以边应该是 bi -> ai
            graph[pre[1]].push_back(pre[0]);
            inDegree[pre[0]]++;  // ai的入度增加
        }

        // 4. 初始化队列，将所有入度为0的课程加入
        queue<int> q;
        for (int i = 0; i < numCourses; i++) 
        {
            if (inDegree[i] == 0) 
            {
                q.push(i);  // 没有前置要求的课程可以直接学习
            }
        }

        int count = 0;  // 记录已完成的课程数量
        vector<int> ret;  // 存储学习顺序

        // 5. BFS处理队列
        while (!q.empty()) 
        {
            int current = q.front();
            q.pop();
            count++;
            ret.push_back(current);  // 完成当前课程

            // 6. 处理当前课程的所有后继课程
            for (int neighbor : graph[current]) 
            {
                // 减少后继课程的入度（因为一个前置课程已完成）
                if (--inDegree[neighbor] == 0) 
                {
                    // 如果入度变为0，说明所有前置课程都已完成
                    q.push(neighbor);
                }
            }
        }

        // 7. 检查是否所有课程都能完成
        // 如果count == numCourses，说明无环，可以完成所有课程
        // 否则说明存在循环依赖，无法完成
        return count == numCourses ? ret : vector<int>();
    }
};


//3.火星词典
//https://leetcode.cn/problems/Jf1JuT/description/
//现有一种使用英语字母的外星文语言，这门语言的字母顺序与英语顺序不同。
//给定一个字符串列表 words ，作为这门语言的词典，words 中的字符串已经 按这门新语言的字母顺序进行了排序 。
//请你根据该词典还原出此语言中已知的字母顺序，并 按字母递增顺序 排列。若不存在合法字母顺序，返回 "" 。若存在多种可能的合法字母顺序，返回其中 任意一种 顺序即可。
//字符串 s 字典顺序小于 字符串 t 有两种情况：
//在第一个不同字母处，如果 s 中的字母在这门外星语言的字母顺序中位于 t 中字母之前，那么 s 的字典顺序小于 t 。
//如果前面 min(s.length, t.length) 字母都相同，那么 s.length < t.length 时，s 的字典顺序也小于 t 。
//示例 1：
//输入：words = ["wrt", "wrf", "er", "ett", "rftt"]
//输出："wertf"
//示例 2：
//输入：words = ["z", "x"]
//输出："zx"
//示例 3：
//输入：words = ["z", "x", "z"]
//输出：""
//解释：不存在合法字母顺序，因此返回 ""。
//提示：
//1 <= words.length <= 100
//1 <= words[i].length <= 100
//words[i] 仅由小写英文字母组成

//构建有向图：通过比较相邻单词，找到第一个不同的字符对，建立字符之间的顺序关系
//检测冲突：如果出现"abc"在"ab"之前的情况，说明顺序无效
//拓扑排序：使用Kahn算法进行拓扑排序，得到字母顺序
//图构建：
//比较相邻单词 words[i] 和 words[j]
//找到第一个不同的字符对(a, b)，建立边 a → b
//表示在外星语言中，a 在 b 之前
//无效情况检测：
//如果 "abc" 在 "ab" 之前，这是无效的（因为前缀相同但长的在前）
//这种情况在 add 函数中通过 check = false 标记
//拓扑排序：
//使用队列处理入度为0的节点
//逐步移除节点并更新邻居的入度
//如果最后还有节点的入度不为0，说明存在环，顺序无效
class Solution {
public:
    unordered_map<char, unordered_set<char>> graph;  // 邻接表表示的有向图
    unordered_map<char, int> inDegree;              // 每个节点的入度
    bool check = true;                              // 有效性检查标志

    string alienOrder(vector<string>& words) 
    {
        int n = words.size();

        // 步骤1：初始化所有字符的入度为0
        for (int i = 0; i < n; i++) 
        {
            for (auto& ch : words[i])
                inDegree[ch] = 0;
        }

        // 步骤2：构建图 - 比较相邻单词
        for (int i = 0; i < n; i++)
            for (int j = i + 1; j < n; j++) 
            {
                add(words[i], words[j]);  // 添加边
                if (!check) return "";    // 发现无效情况立即返回
            }

        // 步骤3：拓扑排序 - Kahn算法
        queue<char> q;
        // 将所有入度为0的节点加入队列
        for (auto& [a, b] : inDegree) 
        {
            if (b == 0) q.push(a);
        }

        string ret;
        while (q.size()) 
        {
            char t = q.front();
            q.pop();
            ret += t;  // 添加到结果中

            // 处理当前节点的所有邻居
            for (auto& ch : graph[t]) 
            {
                if (--inDegree[ch] == 0) q.push(ch);
            }
        }

        // 步骤4：检查是否有环（存在入度不为0的节点）
        for (auto& [a, b] : inDegree)
            if (b) return "";

        return ret;
    }

    void add(string& s1, string& s2) 
    {
        int n = min(s1.size(), s2.size());
        int i = 0;

        // 找到第一个不同的字符
        while (i < n) 
        {
            if (s1[i] != s2[i]) 
            {
                char a = s1[i], b = s2[i];

                // 避免重复添加边
                if (!graph.count(a) || !graph[a].count(b)) 
                {
                    graph[a].insert(b);
                    inDegree[b]++;
                }
                break;
            }
            i++;
        }

        // 特殊情况检查：如果前面所有字符都相同，但s1比s2长
        // 例如：["abc", "ab"] 这种情况无效
        if (i == n && i < s1.size()) check = false;
    }
};